Now it's time to explore how we can make use of some of the other subroutines that are remarkably well-hidden within the ROM. Specifically we'll cover two of these subroutines, which between them will enable us to scan the keyboard and locate which, if any, of the keys on the keyboard are being depressed. On the NEW ROM we can make use of these subroutines just by calling them, but we can't on the OLD ROM because they're simply not there. For the benefit of the people with OLD ROMs I shall include a section at the end of this chapter explaining how these programs may be made to work by actually inputting these subroutines yourselves. This section will also be of interest to those of you with NEW ROMs, since it will give you an insight into how the subroutines actually work.
The first such subroutine is an amazing little keyboard scan, which begins at address 02BB. It may be accessed simply by calling that address, i.e. CALL KSCAN, or CDBB02 in hex. It doesn't actually produce a very usable answer though. Let's see exactly what it does do.
It returns a value to the HL register pair. Actually it returns seperate and independent values - one to H and one to L. Here's how the value of L is interpreted:
Imagine the keyboard (excluding SHIFT) divided up into eight horizontal sections, each containing five keys (except for section zero which only contains four, because SHIFT is ommitted). Notice how each section has a corresponding number between zero and seven. Now, if there is no key depressed then L will return a value FF. However, if one or more keys are depressed, then the appropriate BIT (of L) will be reset to zero. In other words, if you are pressing Q, W, E, R, or T then bit 2 will be reset - if you are pressing B, N, M, full-stop, or space, then bit 7 will be reset. This means that L can return the following:
BINARYHEX
If no key is depressed 11111111 FF
If a section 0 key is depressed 11111110 FE
If a section 1 key is depressed 11111101 FD
If a section 2 key is depressed 11111011 FB
If a section 3 key is depressed 11110111 F7
If a section 4 key is depressed 11101111 EF
If a section 5 key is depressed 11011111 DF
If a section 6 key is depressed 10111111 BF
If a section 7 key is depressed 01111111 7F
+------------------------------+------------------------------+
| SECTION 3 | SECTION 4 |
| ( 1 ) ( 2 ) ( 3 ) ( 4 ) ( 5 )|( 6 ) ( 7 ) ( 8 ) ( 9 ) ( 0 ) |
+-+----------------------------+-+----------------------------+-+
| SECTION 2 | SECTION 5 |
| ( Q ) ( W ) ( E ) ( R ) ( T )|( Y ) ( U ) ( I ) ( O ) ( P ) |
+-+----------------------------+-+----------------------------+-+
| SECTION 1 | SECTION 6 |
| ( A ) ( S ) ( D ) ( F ) ( G )|( H ) ( J ) ( K ) ( L ) (ENT) |
+--+-----------------------+---+--------------------------+---+
| SECTION 0 | SECTION 7 |
(SHF)|( Z ) ( X ) ( C ) ( V )|( B ) ( N ) ( M ) ( . ) ( ) |
+-----------------------+------------------------------+
As an exercise see if you could work out what L would be if both S and P were depressed at the same time.
The value returned by H is determined by a similar principle, but notice how the keyboard is divided up here - not horizontally but vertically. Notice also that the SHIFT key has a section all to itself - section 0. Now if you press key S for instance then H will return FB (in binary 11111011). We have already seen that L would give FB as well, so that HL returns FBFB. Can you see why it is impossible for this value to be obtained from any other key?
1 2 3 4 5 5 4 3 2 1
+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
\ \ \ \ \ \ \ \ \ \ \
\( 1 )\( 2 )\( 3 )\( 4 )\( 5 )\( 6 )\( 7 )\( 8 )\( 9 )\( 0 )\
\ \ \ \ \ \ \ \ \ \ \
\ \ \ \ \ \ \ \ \ \ \
\( Q )\( W )\( E )\( R )\( T )\( Y )\( U )\( I )\( O )\( P )\
\ \ \ \ \ \ \ \ \ \ \
\ \ \ \ \ \ \ \ \ \ \
\( A )\( S )\( D )\( F )\( G )\( H )\( J )\( K )\( L )\(ENT)\
+-----+ + + + + + + + + +
+-----+ / / / / / / / / / /
|(SHF)| /( Z )/( X )/( C )/( V )/( B )/( N )/( M )/( . )/( )/
+-----+ +-----+-----+-----+-----+-----+-----+-----+-----+-----+
0 2 3 4 5 5 4 3 2 1
N N N N N N N N N N
O O O O O O O O O O
I I I I I I I I I I
T T T T T T T T T T
C C C C C C C C C C
E E E E E E E E E E
S S S S S S S S S S
Now let's see what would happen if you pressed SHIFT S. Both bits zero and two would be reset giving, in binary, 11111010. In hex this is FA, so HL would return as FAFB - which is different to the value produced without shift. We can see the precise effect of SHIFT from the table:
WITHOUT SHIFTWITH SHIFTBINARYHEXBINARYHEX
If no key is depressed 11111111 FF 11111110 FE
If a section 1 key is depressed 11111101 FD 11111100 FC
If a section 2 key is depressed 11111011 FB 11111010 FA
If a section 3 key is depressed 11110111 F7 11110110 F6
If a section 4 key is depressed 11101111 EF 11101110 EE
If a section 5 key is depressed 11011111 DF 11011110 DE
It should by now be reasonably clear how each individual key, with or without shift, produces its own unique code in the HL register pair. If two keys are both in the same horizontal section they cannot possibly both be in the same vertical section. Note that SHIFT on its own returns a value of FEFF which is not the same as no key depression at all.
The subroutine which I've called KSCAN does have one big disadvantage though - it will completely wipe out the previous values of all the registers! If you want to preserve them you'll have to make use of the stack as follows:
F5 PUSH AF
C5 PUSH BC
D5 PUSH DE
CDBB02 CALL KSCAN
D1 POP DE
C1 POP BC
F1 POP AF
Now we want to turn these rather obscure numbers into real character codes. It just so happens that all of these codes are rather cleverly stored in the ROM beginning at address 007E. By "rather cleverly" I mean in a most convenient order, as follows: First the straightforward characters:
(There are no spaces between the characters - they are printed here to make the ordering more obvious.) Then the shift characters:
00A5 :;?/ STOP LPRINT SLOW FAST LLIST "" OR STEP <= <> EDIT AND THEN
TO cursor-left RUBOUT GRAPHICS cursor-right cursor-up cursor-down
")($ >= FUNCTION =+- ** £,><*
Can you see how the ordering relates to which sections the key lies in? We could quite easily write a subroutine now to convert from the strange number we already have in HL to an address between 007E and 00CB (the last item in the table - the *) but it turns out that we don't need to because that nice man Uncle Clive has already done it for us with a subroutine which I shall call FINDCHR beginning at 07BD. The ROM people probably have their own name for it but they keep it shrouded in mystery. The subroutine performs the following task - given a value as defined above, in the BC register, it will work out the address at which the appropriate character is stored - the final result ending up in HL.
It does have a problem though. If you're not pressing a key then surely you shouldn't end up with a character to print! You'll have to prevent this yourself. One way would be as follows. Notice though how we move the result from the first subroutine into the BC register before calling the second.
CDBB02 START CALL KSCAN
44 LD B,H
4D LD C,L
51 LD D,C
14 INC D
3E00 LD A,00
2804 JR Z,NOCHR
CDBD07 CALL FINDCHR
7E LD A,(HL)
... NOCHR ...
rest of program
There are several things to note about this example. Firstly that two seperate instructions, LD B,H and LD C,L are needed to transfer HL to BC since there is no single instruction LD BC,HL. Secondly that the condition JR Z means if D is zero, not A - LD does not alter any of the flags. If D is zero after being incremented then it must have been FF beforehand, which means that L must have been FF after it came out of the first subroutine. This is the check that a key is being pressed. A is loaded with zero and if no key is pressed it remains zero, otherwise it takes on the code of whichever character you're touching on the keyboard.
There is here a slight ambiguity in that the zero is also produced if you press space. You could use LD A,01 instead of LD A,00 since the character whose code is one () is not available from the keyboard. Now there is no ambiguity since zero means space and one means no character is being pressed. If you have SLOW at your disposal you could omit LD A,00 altogether and use JR Z,START instead of JR Z,NOCHR. Now the program will WAIT until a key is pressed before continuing. Without SLOW it will still wait but you'll have to suffer a blank screen in the meantime.
The A register now contains a value corresponding precisely to the function INKEY$. In this way real time games are just as feasible in machine code as they are in BASIC.
Another interesting part of the ROM is the very last bit - the half of a K that runs from 1E00 to 1FFF. It's not a subroutine, it's a table - a very long table - actually the longest table in ROM. It stores the dot pattern of every symbol used by the computer - that is all of the printable characters. It takes eight bytes to store a single character symbol, so for example, the characters A, B and C are represented, in binary, by:
Can you pick the letters A, B and C from the digits above? The pattern is held by the positions of the "ones" amongst the "zeroes". When they finally appear on your TV screen they look like this:
Suppose we now wished to reconstruct these letters in an enlarged form - using a pixel (quarter-square) for each dot. This means that each character we print should be a graphics character (space and inverse-space both count as graphics characters) chosen so that the correct quarters are black.
There are two ways of doing this. One is to make use of the NEW ROM character codes, in which the graphics are arranged in a very clever order - unfortunately we would not be able to adapt this system to the OLD ROM. The second is to include sixteen bytes of data within our program representing the graphics symbols in any order we care to choose. Let's take a look at the first method first.
Suppose the bottom right corner is WHITE. If we give the other pixels numbers 1, 2 and 4 then simply adding them up gives the required character code. You can check this by comparing the diagram below with the character set in the Sinclair manual.
If the bottom right hand corner is BLACK then we need to give the other pixels the numbers -1, -2, and -4. To work out the code of any graphics symbol here we start off with the number 135 (decimal) and subtract appropriately the required number for each black pixel. Again you can check this by comparing the diagram below with the Sinclair manual.
Incidently it is worth pointing out here that many copies of the Sinclair manual incorrectly give the character of 135 as . This is a misprint - it should of course be . Try typing PRINT CHR$ 135 to check. Character seven is - the manual gives this correctly.
[Thunor: The author is saying that if the bottom-right is white then you add-up the values in the other squares if they are black, and if the bottom-right is black then you subtract the values in the other squares from 135 if they are black. A quick peek at the NEW ROM character set in appendix five illustrates this.]
The character codes of the OLD ROM graphics symbols are unfortunately rather random, so there is no single system for working out the code, given which pixels should be black and which should be white. In order that the program to follow should work on both ROMs we shall adopt a slightly different method. Instead of distinguishing two different cases (that is the colour of the bottom right-hand pixel) we shall treat every quarter-square the same, and code them as follows:
We should then have to include in our program a DATA section which lists the graphics symbols in the order
Move RAMTOP to address 4380 (this is a hex address) by typing POKE 16388,128 POKE 16389,67 then NEW. Now load the following program to address 4380 (in decimal this is 17280, meaning 1K users will still be able to run it). As it stands this program is best run in SLOW. We shall see how to alter it so that it will run in FAST later.
[Thunor: Unless you really want this to run in 1K I suggest you don't set RAMTOP this low because you're going to want to use HEXLD2 or HEXLD3 to type it in and SAVE it. I recommend HEXLD3 -- seeing as you've just written it or downloaded mine -- and writing to 4D00h (19712d), therefore the instruction 218043 LD HL,DATA below should read 21004D LD HL,DATA, and to start it type RAND USR 19728. NEW ROM users don't forget that HEXLD3 requires initialising for a new program.]
[NEW ROM ONLY]
00870483 DATA DEFM "" This is the table of
02850681 DEFM "" graphics symbols in
01860582 DEFM "" the required order.
03840780 DEFM ""
[OLD ROM ONLY]
00070603 DATA DEFM "" This is the table of
05820884 DEFM "" graphics symbols in
04880285 DEFM "" the required order.
83868780 DEFM ""
[Both ROMs Continued]
CDBB02 START CALL KSCAN Wait for human to take
2C INC L finger off of key.
20FA JR NZ,START
CDBB02 WAIT CALL KSCAN Wait for new key to be
44 LD B,H pressed.
4D LD C,L
51 LD D,C
14 INC D
28F7 JR Z,WAIT
CDBD07 CALL FINDCHR Locate appropriate
7E LD A,(HL) character code.
A7 AND A
17 RLA Multiply by eight, but
17 RLA return to BASIC if a non-
D8 RET C printable character is
17 RLA pressed.
1600 LD D,00
CB12 RL D
5F LD E,A
21001E LD HL,CTABLE Find start of dot <- NEW ROM ONLY
21000E LD HL,CTABLE Find start of dot <- OLD ROM ONLY
19 ADD HL,DE pattern.
0E04 LD C,04
0604 OUTERLOOP LD B,04
56 LD D,(HL) Transfer two lines of dots
23 INC HL into D and E
5E LD E,(HL)
23 INC HL
E5 PUSH HL
AF INNERLOOP XOR A Compute which graphics
CB12 RL D character is to be
17 RLA printed.
CB12 RL D
17 RLA
CB13 RL E
17 RLA
CB13 RL E
17 RLA
218043 LD HL,DATA Get this character from
85 ADD A,L the table of graphics
6F LD L,A symbols.
7E LD A,(HL)
D9 EXX Print this symbol.
F5 PUSH AF <- OLD ROM ONLY
CDE006 CALL PRPOS <- OLD ROM ONLY
F1 POP AF <- OLD ROM ONLY
CD2007 CALL PRINT <- OLD ROM ONLY
CD0808 CALL PRINT <- NEW ROM ONLY
D9 EXX
10E6 DJNZ INNERLOOP Next print <- NEW ROM ONLY
10E1 DJNZ INNERLOOP Next print <- OLD ROM ONLY
E1 POP HL position.
3E76 LD A,76 End of current line.
D9 EXX
F5 PUSH AF <- OLD ROM ONLY
CDE006 CALL PRPOS <- OLD ROM ONLY
F1 POP AF <- OLD ROM ONLY
CD2007 CALL PRINT <- OLD ROM ONLY
CD0808 CALL PRINT <- NEW ROM ONLY
D9 EXX
0D DEC C
20D4 JR NZ OUTERLOOP Next line begins. <- NEW ROM ONLY
20CA JR NZ OUTERLOOP Next line begins. <- OLD ROM ONLY
18AF JR START Start again. <- NEW ROM ONLY
C9 RET <- OLD ROM ONLY
The program is now complete. Make sure you are in SLOW mode and start the program off by typing RAND USR 17296. DO NOT type RAND USR 17280 since this is purely data and will not run. You should see a completely blank screen. Press "C", and watch what happens. Now press "A". Interesting isn't it? Try experimenting with different keys to see what happens - and what happens when you run out of screen?
You may have been confused by the use of the instruction EXX which was used four times in the above program. Its function is very easy to explain. As you know, the registers B, C, D, E, H, L, and A can [be] very easily manipulated, but there are also seven other registers, called B', C', D', E', H', L', and A' (pronounced A-dash or A-prime). These are not so easy to manipulate and can in practice only be used for storage purposes. The instruction EXX means exchange B and B', C and C', D and D', E and E', H and H', L and L'. Thus all the registers except A lose their previous values but take on the values of their alternative registers. Likewise the alternative registers take on the original values of the usual registers.
The reason we need to do this is because the ROM subroutine PRINT destroys the previous values of BC, DE, and HL. We could have preserved them by pushing them onto the stack, but EXX works just as well here and is only one instruction.
Lets take a closer look at the above program and sort out exactly what each bit does. First of all we find the right character code, which gets stored in the A register. The instruction AND A resets the carry flag to zero. RLA will then multiply A by two. Now we know that this character is on the keyboard and can be obtained in one touch, so it is not an inverse character. Rotating left then will move the leftmost bit, which must be a zero, into the carry flag. RLA a second time will again multiply by two (since we know the carry is zero), however, if the character is NOT PRINTABLE (such as newline or STOP) then bit 6 of the original value will be a one. This will now be moved into carry. The instruction RET C ensures that if this circumstance ever occurs the program will terminate.
Knowing then that the carry is still zero we can use RLA once more to multiply by two. Here however, bit 5, which [is] a necessary part of the character code, will be moved into the carry flag. To move the carry digit into D we use two instructions LD D,00 and RL D. D will then contain either zero or one. LD E,A ensures that register-pair DE now contains eight times the original value of A.
The other interesting part is the first nine lines of the INNER-LOOP. A is loaded with the first two bits of D and the first two bits of E. This gives a number between zero and fifteen which corresponds to the required graphics symbol. It is NOT the character code, it is the specially designed code we worked out earlier on. Notice how the NEXT bits of D and E are now automatically in place at the extreme left.
For those of you who do not have SLOW I suggest replacing the last instruction, JR START by RET. You could then have a surrounding BASIC program as follows:
10 RAND USR 17296
20 PAUSE 40000 <- NEW ROM ONLY
20 INPUT A$ <- OLD ROM ONLY
30 RUN
THE SUBROUTINES
OLD ROM users will by now be feeling quite envious at NEW ROM people for having these subroutines at their disposal. Of course there is a keyboard scan in the OLD ROM, but it isn't a subroutine - i.e. it doesn't end in RET. One call to it and you're stuck there forever! What we'll have to do is rewrite them ourselves. We can do this by taking all of the important bits from the subroutines in the NEW ROM.
First of all KSCAN. This is the required subroutine. Don't worry if there are parts of it you don't understand - all will become clear in due course.
21FFFF KSCAN LD HL,FFFF
01FEFE LD BC,FEFE
ED78 IN A,(C)
F601 OR 01
F6E0 LOOP OR E0
57 LD D,A
2F CPL
FE01 CP 01
9F SBC A,A
B0 OR B
A5 AND L
6F LD L,A
7C LD A,H
A2 AND D
67 LD H,A
CB00 RLC B
ED78 IN A,(C)
38ED JR O,LOOP
1F RRA
CB14 RL H
C9 RET
Now - if you enter this subroutine into RAM you can then replace every CDBB02 in the chapter by a call to the appropriate address in RAM. The other subroutine you'll need to be able to emulate is FINDCHR. This may be done as follows.
1600 FINDCHR LD D,00
CB28 SRA B
9F SBC A,A
F626 OR 26
2E05 LD L,05
95 SUB L
85 LOOP ADD A,L
37 SCF
CB19 RR C
38FA JR C,LOOP
0C INC C
C0 RET NZ
48 LD C,B
2D DEC L
2E01 LD L,01
20F2 JR NZ,LOOP
217D00 LD HL,KTABLE-1 <- NEW ROM ONLY
216B00 LD HL,KTABLE-1 <- OLD ROM ONLY
5F LD E,A
19 ADD HL,DE
C9 RET
The address 007D, referred to in my listing as KTABLE-1, is for the NEW ROM only. THE ADDRESS OF KTABLE IN THE OLD ROM IS 006C, and so this line should be changed to LD HL,006B. This is far easier to understand than the first subroutine. The second and third lines are rather interesting.
If you remember BC should contain a code corresponding to one of the keys at the start of the subroutine. Now bit zero of B is a one if SHIFT is not pressed, and zero if shift is pressed. SRA B will shift B to the right, will set bit 7 to one (do you remember the difference between SRA and SRL?), and will set the carry flag equal to the previous value of bit zero.
SBC A,A will first of all subtract A from A - effectively resetting it to zero - and will then subtract the carry flag. In other words, if SHIFT is pressed A will end up as 00, if SHIFT is not pressed A will end up as FF.
The fourth line, OR 26, will ensure that A is 26 for a shifted character, FF for a non-shifted character.
You should recall here that B contains information about which VERTICAL section the key is in, and C about which HORIZONTAL section. If you take a closer look at the order the characters are stored [in] in the keyboard table (KTABLE) which was shown a few pages back you'll see that the horizontal-section-number needs to be multiplied by five, and the vertical-section-number added to it, in order to find a specific key in the table. This is what the next part does:
L is loaded with 5 - the multiplying factor. Notice how the next two lines cancel each other out the first time round the loop. This is one way of adding L nought times should we need to. The next two lines are SCF and RR C. This is not the same thing as SRA C, since bit 7 could be zero (i.e. if a horizontal-section-7 key is pressed). Apart from shifting C to the right it also moves one bit into the carry. If this bit is a one we haven't found the right section yet and the loop is re-executed. Note that five is added each time round the loop. Note also that if A starts off as FF it is just as easy, if not easier, to think of it as minus-one.
Now that we're out of the loop, C should be all ones, that is, it should be FF, so that INC C should ensure that it becomes zero, so what's this RET NZ instruction for? Of course this condition is simply to check that you're not holding down two keys at once. What would C contain if you were?
LD C,B moves the vertical-section-data into the B register, so that the same loop may be used over again.
DEC L followed by LD L,1 looks confusing. Actually it's not. At the moment L is five, and so DEC L makes it four, which is NOT ZERO. LD L,1 doesn't alter the zero flag, so JR NZ,LOOP sends it back through the same loop, but this time checking the vertical sections, and only incrementing by one instead of five.
When it comes out of the loop DEC L will reduce to zero, so after reloading L with one JR NZ will not be satisfied and the program will continue.
LD HL,KTABLE-1. Why minus one? Well if there was a "real key" in the position where SHIFT is and you were pressing it then A would end up as zero. Since there isn't the smallest value A can end up as is one, which happens if you hold down "Z", hence LD HL,KTABLE-1 takes this into account.
LD E,A is effectively loading A into DE. This works because D is already zero - see the first line of the program. Then ADD HL,DE will find the correct address. Notice that we could have replaced these two instructions by ADD A,L followed by LD L,A. This has the advantage that the first instruction (LD D,00) becomes unnecessary, and that DE is not at all altered by the subroutine. The ROM however uses the version as listed.
KTABLE in the OLD ROM looks like this:
006C ZXCV ASDFG QWERT 12345 09876 POIUY newline LKJH space .MNB
0093 :;?/ NOT AND THEN TO cursor-left RUBOUT HOME
cursor-right cursor-up cursor-down *)($" edit =+- ** £,>< OR
For the actual printing process itself, the instruction CALL PRINT for the NEW ROM should be replaced by PUSH AF/CALL PRPOS/POP AF/CALL PRINT. In HEX this is F5/CDE006/F1/CD2007.
The Character Table (CTABLE) which gives the dot patterns for the characters is located in the OLD ROM at address 0E00, rather than 1E00. Again it is stored at the very end of the ROM. All of the characters are slightly different.
The data for the table of graphics symbols in the character printing program should run 00 07 06 03 05 82 08 84 04 88 02 85 83 86 87 80 if the program is to be used with an OLD ROM. Replace the PAUSE 40000 BASIC statement given in the following text [previous listing] by INPUT A$.
[Download available for 16K ZX81 -> chapter10-enlarge2.p. Addresses used: DATA is 4D00, START is 4D10, KSCAN is 4D61 and FINDCHR is 4D82.]
[Download available for 16K ZX80 -> chapter10-enlarge.o. Addresses used: 4A1A to 4B3A is occupied by HEXLD3, DATA is 4B3B, START is 4B4B, KSCAN is 4BA5 and FINDCHR is 4BC6. Unfortunately I can't get this version to work. There's a possibility I may return to this to see if I can fix it, but I've already spent a lot of time checking it over. Feel free to send me your working version and I'll credit you in the page :)]
GRAFFITTI
It only requires a slight modification to the original version in order to make a really excellent program, demonstrating the immense speed which machine code offers over BASIC. In this program, GRAFFITTI, you touch a key and an enlarged version of the required symbol appears on the screen. In this program the whole screen is used (even the bottom two lines) thus allowing a total of forty-eight symbols on the screen. To load it move RAMTOP to any address not less than 4D00 and NEW (i.e. this can't be done in 1K - at least not in this version). The program is as follows:
2A0C40 GRAFFITTI LD HL,(D_FILE) Set the print position
23 INC HL to the start of the screen.
220E40 LD (DF_CC),HL
36B0 START LD (HL),B0 Print a cursor.
CDBB02 PAUSE CALL KSCAN Wait for human to take
2C INC L finger off of key.
20FA JR NZ,PAUSE
CDBB02 WAIT CALL KSCAN Wait for new key to
44 LD B,H be pressed.
4D LD C,L
51 LD D,C
14 INC D
28F7 JR Z,WAIT
CDBD07 CALL FINDCHR Locate the correct
7E LD A,(HL) character code.
A7 AND A
17 RLA Multiply by eight, but
17 RLA return to BASIC if a
D8 RET C non-printable character
17 RLA is pressed.
1600 LD D,00
CB12 RL D
5F LD E,A
21001E LD HL,CTABLE Find start of dot <- NEW ROM ONLY
21000E LD HL,CTABLE Find start of dot <- OLD ROM ONLY
19 ADD HL,DE pattern.
0E04 LD C,04
0604 OUTERLOOP LD B,04
56 LD D,(HL) Transfer two lines of
23 INC HL dots into D and E.
5E LD E,(HL)
23 INC HL
E5 PUSH HL
AF INNERLOOP XOR A Computer which graphics
CB12 RL D character is to be
17 RLA printed.
CB12 RL D
17 RLA
CB13 RL E
17 RLA
CB13 RL E
17 RLA
21-DATA-address LD HL,DATA Get this character from
85 ADD A,L the table of graphics
6F LD L,A symbols.
7E LD A,(HL)
2A0E40 LD HL,(DF_CC) Print this character
77 LD (HL),A in required position.
23 INC HL Store new print
220E40 LD (DF_CC),HL position.
10E3 DJNZ INNERLOOP
D5 PUSH DE Move print position
111D00 LD DE,001D ready for next row
19 ADD HL,DE of symbols.
220E40 LD (DF_CC),HL
D1 POP DE
E1 POP HL
0D DEC C
20CF JR NZ,OUTERLOOP
1180FF LD DE,FF80 Move print position
2A0E40 LD HL,(DF_CC) ready for next enlarged
19 ADD HL,DE character.
220E40 LD (DF_CC),HL
7E LD A,(HL) Check for end of line.
FE76 CP 76
209B JR NZ,START
116400 LD DE,0064 Move print position to
19 ADD HL,DE left of screen ready for
220E40 LD (DF_CC),HL next enlarged character.
23 INC HL
ED5B1040 LD DE,(VARS) Check for end of screen.
ED52 SBC HL,DE
19 ADD HL,DE
383A JR C,START
C9 RET
[Download available for 16K ZX81 -> chapter10-graffitti.p. Addresses used: DATA is 4D00, GRAFFITTI is 4D10, KSCAN is 4D8E and FINDCHR is 4DAF. It's supposed to fill the entire screen but it only wraps one line. When I have some spare time I'll return to this and attempt to fix it.]
This program is intended to be run on a ZX81 in the SLOW mode. See if you can work out how to adapt it so that it will print inverse characters instead of ordinary ones. It may even be possible to offer a choice!